package com.hww.slow_sql;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.hww.slow_sql.mapper.SlowSQLMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;

/**
 * 慢SQL处理器
 *
 * @author xiaojie
 * @date 2021/1/9 1:57 上午
 */
@Slf4j
@Component
public class SlowSQLProcessor implements InitializingBean {

    /**
     * 慢SQL信息队列
     */
    private final LinkedBlockingQueue<SlowSQLDTO> slowSQLQueue = new LinkedBlockingQueue<SlowSQLDTO>();

    /**
     * 线程调度池
     */
    private ScheduledExecutorService scheduledExecutorService;

    /**
     * 单个调度器
     */
    private ScheduledFuture<?> scheduledFuture;


    /**
     * 慢SQL信息业务
     */
    @Autowired
    private SlowSQLMapper slowSQLService;

    /**
     * 添加慢SQL信息
     */
    public void addSlowSQLInfo(SlowSQLDTO slowSQLDTO) {
        // 校验下是不是慢SQL自己的持久化, 自己的持久化不做处理
        if (!isSlowSQLProcessThis(slowSQLDTO)) {
            this.slowSQLQueue.add(slowSQLDTO);
        }
    }

    /**
     * 校验下是不是自己, 是自己的话, 不处理, 防止死循环
     */
    protected boolean isSlowSQLProcessThis(SlowSQLDTO slowSQLDTO) {
        final Object[] args = slowSQLDTO.getInvocation().getArgs();
        final MappedStatement ms = (MappedStatement) args[0];
        String msId = ms.getId();
        return msId.startsWith(SlowSQLConfig.SLOW_SQL_MAPPER);
    }

    /**
     * 报告慢SQL
     */
    protected void reportSlowSQL() throws Exception {
        SlowSQLDTO tmpSlowSQL = this.slowSQLQueue.poll();
        if (tmpSlowSQL == null) {
            return;
        }
        // 获取参数信息
        final Object[] args = tmpSlowSQL.getInvocation().getArgs();
        final MappedStatement ms = (MappedStatement) args[0];
        final Object param = args[1];
        final BoundSql boundSql = ms.getBoundSql(param);
        final String sql = boundSql.getSql();
        final String paramJson = boundSql.getParameterObject() == null ? "" : JSON.toJSONString(boundSql.getParameterObject());

        // 插入数据库
        SlowSQLDO slowSQLDO = new SlowSQLDO();
        try {
            // 保存慢SQL, 信息入库, 方便后续排查
            slowSQLDO.setSqlType((tmpSlowSQL.getException() != null ? 1 : (tmpSlowSQL.getQueryRowCount() > 0 ? -1 : 0)));
            slowSQLDO.setMthName(ms.getId());
            slowSQLDO.setSql(sql);
            slowSQLDO.setParams(paramJson + ", traceId[" + tmpSlowSQL.getTraceId() + "]");
            slowSQLDO.setTime((tmpSlowSQL.getEnd() - tmpSlowSQL.getStart()));
            slowSQLDO.setException(getErrorMsg(tmpSlowSQL.getException()));
            slowSQLDO.setGmtCreate(new Date());
            slowSQLDO.setGmtModify(new Date());
            slowSQLDO.setId(-1L);
            this.slowSQLService.saveSlowSQL(slowSQLDO);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }

        // 执行异常打印
        StringBuffer msgBuf = new StringBuffer();
        if (tmpSlowSQL.getException() != null) {
            // 异常信息
            msgBuf.append("SQL执行异常, ")
                    .append("记录ID[").append(slowSQLDO.getId()).append("], ")
                    .append("traceId[").append(tmpSlowSQL.getTraceId()).append("], ")
                    .append("方法[").append(ms.getId()).append("], ")
                    .append("SQL[").append(sql).append("], ")
                    .append("参数[").append(paramJson).append("], ")
                    .append("异常[").append(slowSQLDO.getException()).append("].");
            // 打印日志
            log.error(msgBuf.toString());
        } else {
            // 慢SQL, 防止替换预编译失败, 预编译存在失败的可能性
            try {
                msgBuf.append(tmpSlowSQL.getQueryRowCount() < 0 ? "慢SQL, " : "大SQL, ")
                        .append("记录ID[").append(slowSQLDO.getId()).append("], ")
                        .append("traceId[").append(tmpSlowSQL.getTraceId()).append("], ")
                        .append("执行耗时[").append((tmpSlowSQL.getEnd() - tmpSlowSQL.getStart())).append("]毫秒, ")
                        .append(tmpSlowSQL.getQueryRowCount() > 0 ? "结果条数[" + tmpSlowSQL.getQueryRowCount() + "]条, " : "")
                        .append("方法[").append(ms.getId()).append("], ")
                        // .append("预编译SQL[").append(sql).append("], ")  // 数据多了, 钉钉推送不了那么多
                        .append("处理后SQL[").append(replacePrepareParam(sql, tmpSlowSQL.getInvocation())).append("], ")
                        .append("参数[").append(paramJson).append("].");
                // 打印日志
                log.info(msgBuf.toString());
            } catch (Exception e) {
                // 防止参数转换出错, 导致数据出错, 进行下容错
                log.error("SQL预编译替换出错, 方法[" + ms.getId() + "], 参数[" + paramJson + "].", e);
                msgBuf = new StringBuffer();
                msgBuf.append(tmpSlowSQL.getQueryRowCount() < 0 ? "慢SQL, " : "大SQL, ")
                        .append("记录ID[").append(slowSQLDO.getId()).append("], ")
                        .append("traceId[").append(tmpSlowSQL.getTraceId()).append("], ")
                        .append("执行耗时[").append((tmpSlowSQL.getEnd() - tmpSlowSQL.getStart())).append("]毫秒, ")
                        .append(tmpSlowSQL.getQueryRowCount() > 0 ? "结果条数[" + tmpSlowSQL.getQueryRowCount() + "]条, " : "")
                        .append("方法[").append(ms.getId()).append("], ")
                        .append("预编译SQL[").append(sql).append("], ")
                        .append("预编译失败异常[").append(e.getMessage()).append("], ")
                        .append("参数[").append(paramJson).append("].");
                // 打印日志
                log.error(msgBuf.toString());
            }
        }
    }

    /**
     * 转换参数, 替换参数
     * 参照 org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
     */
    protected String replacePrepareParam(String prepareSQL, Invocation invocation) {

        final Object[] args = invocation.getArgs();
        final MappedStatement ms = (MappedStatement) args[0];
        final Object parameterObject = args[1];
        final BoundSql boundSql = ms.getBoundSql(parameterObject);
        final String sql = boundSql.getSql();

        // 检查是否存在预编译参数, 不存在预编译参数, 就可以直接返回SQL
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings == null || parameterMappings.size() == 0) {
            return prepareSQL;
        }
        // 考虑类型转换器
        TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
        Configuration configuration = ms.getConfiguration();

        // 参数信息, 处理好的参数信息
        List<Object> params = new ArrayList<Object>();

        // 遍历参数
        if (parameterMappings != null) {
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
                    TypeHandler typeHandler = parameterMapping.getTypeHandler();
                    JdbcType jdbcType = parameterMapping.getJdbcType();
                    if (value == null && jdbcType == null) {
                        jdbcType = configuration.getJdbcTypeForNull();
                    }
                    // 存储好, 参数值
                    params.add(value);
                }
            }
        }
        // 处理SQL替换
        StringBuilder source = new StringBuilder(prepareSQL);
        StringBuilder result = new StringBuilder();
        int sourceindex = 0;
        for (int i = 0; i < source.length(); i++) {
            char ch = source.charAt(i);
            if (ch == '?') {
                result.append(convertParam(params.get(sourceindex++)));
            } else if (ch == '\n' || ch == '\r') {
                result.append(" ");
            } else {
                result.append(ch);
            }
        }
        return result.toString();
    }

    /**
     * 执行参数转换
     */
    protected String convertParam(Object sourceParam) {
        // 参数为空, mybatis 内部会为空指针
        if (sourceParam == null) {
            return "'null'";
        }
        // 判断是否为字符串
        if (CharSequence.class.isAssignableFrom(sourceParam.getClass())) {
            return "'" + sourceParam.toString().trim() + "'";
        }
        // 判断是否为数字
        if (Number.class.isAssignableFrom(sourceParam.getClass())) {
            return sourceParam.toString();
        }
        // 判断集合
        if (Collection.class.isAssignableFrom(sourceParam.getClass())
                || sourceParam.getClass().isArray()) {
            // 转换成json, 删除两头
            String arrJson = JSON.toJSONString(sourceParam);
            return arrJson.substring(1, arrJson.length() - 1);
        }
        // 枚举
        if (Enum.class.isAssignableFrom(sourceParam.getClass())) {
            Enum e = (Enum) sourceParam;
            return e.ordinal() + "";
        }
        // 时间格式处理
        if (sourceParam instanceof Date) {
            return "'" + DateUtil.format((Date) sourceParam, DatePattern.NORM_DATETIME_PATTERN) + "'";
        }
        if (sourceParam instanceof LocalDateTime) {
            LocalDateTime localDateTime = (LocalDateTime) sourceParam;
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
            return "'" + dateTimeFormatter.format(localDateTime) + "'";
        }
        // 布尔值
        if (sourceParam instanceof Boolean) {
            Boolean bl = (Boolean) sourceParam;
            return bl ? "1" : "0";
        }
        return "''";
    }

    /**
     * 获取异常的错误信息, 异常是调用栈信息
     */
    protected String getErrorMsg(Throwable e) {
        try {
            if (ObjectUtil.isEmpty(e)) {
                return "";
            }
            String error = e.getMessage();
            if (ObjectUtil.isEmpty(error)) {
                Throwable pException = e.getCause();
                return getErrorMsg(pException);
            }
            return error;
        } catch (Throwable ex) {
            log.error(ex.getMessage(), ex);
        }
        return "";
    }


    @Override
    public void afterPropertiesSet() throws Exception {
        this.scheduledExecutorService = Executors.newScheduledThreadPool(1, new NamedThreadFactory("slowsql-report-task"));
        this.scheduledFuture = this.scheduledExecutorService.scheduleAtFixedRate(() -> {
            try {
                reportSlowSQL();
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }, 1000, 300, TimeUnit.MILLISECONDS);
    }


    @PreDestroy
    public void close() {
        if (this.scheduledFuture != null) {
            this.scheduledFuture.cancel(true);
            this.scheduledFuture = null;
        }
        if (this.scheduledExecutorService != null) {
            this.scheduledExecutorService.shutdownNow();
            this.scheduledExecutorService = null;
        }
    }
}
